<?php
/**
 * Created by PhpStorm.
 * User: hoter
 * Date: 2016/4/3
 * Time: 11:41
 */

namespace app\core\components;


use yii\base\Exception;
use yii\base\Object;

class WXUtil {

    public $appid;
    public $secret;
    //支付参数
    public $mch_id;
    public $key;

    private $apiURL = "https://api.weixin.qq.com/cgi-bin";
    private $oauthApiURL = "https://api.weixin.qq.com/sns";
    private $requestCodeURL = "https://open.weixin.qq.com/connect/oauth2/authorize";

    public function __construct(){
        $this->appid = 'wx6861e2a2f93e7700';
        $this->secret = '2dded39ac822254cfc1a9ecba29c2b98';
        $this->mch_id = '1482002492';
        $this->key = "g6zJMk6yWAcUCRmJWKsOpT1PB9WNY8ms";
    }

    /**
     * 构建获取用户授权信息的URL
     * @param $redirect_url
     * @param null $state
     * @param string $scope
     * @return string
     */
    public function getRequestCodeURL($redirect_url, $state=null, $scope='snsapi_userinfo'){
        $param = [
            'appid' => $this->appid,
            'redirect_uri' => $redirect_url,
            'response_type' => 'code',
            'scope' => $scope,
        ];
        if(!is_null($state) && preg_match('/[a-zA-z0-9]/', $state)){
            $param['state'] = $state;
        }
        $paramString = http_build_query($param);
        return $this->requestCodeURL.'?'.$paramString.'#wechat_redirect';
    }

    /**
     * @param string $type
     * @param null $code
     * @return mixed
     * @throws \Exception
     */
    /*public function getAccessToken($type='client', $code=null){
        $param = [
            'appid' => $this->appid,
            'secret' => $this->secret
        ];
        switch($type){
            case 'client'://���õ���cache�洢
                $param['grant_type'] = 'client_credential';
                $url = $this->apiURL.'/token';
                break;
            case 'code':
                $param['code'] = $code;
                $param['grant_type'] = 'authorization_code';
                $url = $this->oauthApiURL.'/oauth2/access_token';
                break;
            default:
                throw new \Exception('��֧�ֵ�grant_type����');
                break;
        }
        $result = self::http($url, $param);
        $result = json_decode($result, true);
        if(is_array($result)) {
            if(isset($result['errcode'])) {
                throw new \Exception($result['errmsg']);
            } else {
                return $result;
            }
        }else{
            throw new \Exception('��ȡAccess_Token����');
        }
    }*/

    /**
     * @return mixed
     * @throws \Exception
     */
    public function getAccessToken(){
        $param = [
            'appid' => $this->appid,
            'secret' => $this->secret,
            'grant_type' => 'client_credential',
        ];
        $access_token = \Yii::$app->cache->get('access_token');
        if(!empty($access_token)) {
            return $access_token;
        }
        $url = $this->apiURL.'/token';
        $result = self::http($url, $param);
        $result = json_decode($result, true);
        if (isset($result['access_token'])){
            $expire = $result['expires_in'] ? intval($result['expires_in'])-100 : 3600;
            \Yii::$app->cache->set('access_token', $result['access_token'],$expire);
            return $result['access_token'];
        } else {
            return false;
        }
    }

    /**
     * @param $code string
     * @return mixed
     * @throws \Exception
     */
    public function getAuthAccessToken($code){
        $param = [
            'appid' => $this->appid,
            'secret' => $this->secret,
            'grant_type' => 'authorization_code',
            'code' => $code,
        ];
        $url = $this->oauthApiURL.'/oauth2/access_token';
        $result =  self::http($url,$param);
        $result = json_decode($result, true);
        if(is_array($result)) {
            if(isset($result['errcode'])) {
                //throw new \Exception($result['errmsg']);
                $url = \Yii::$app->urlManager->createUrl(['member/index']);
                header("location: $url");
            } else {
                return $result;
            }
        }else{
            throw new \Exception('获取网页授权Access_Token失败');
        }
    }

    /**
     * 用户网页授权的信息
     * @param string $state  STATE
     * @param string $scope  snsapi_userinfo | snsapi_base
     * @return mixed
     * @throws \Exception
     */
    public function getAuthInfo($scope="snsapi_userinfo",$state="STATE"){
        $code = \Yii::$app->request->get('code');
        $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
        $url = "$protocol$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";
        if (!$code) {
            $url = $this->getRequestCodeURL($url,$state,$scope);
            header("Location: $url");
            exit;
        } else {
            return $this->getAuthAccessToken($code);
        }
    }


    /**
     * @param $token
     * @param $openid
     * @param string $lang
     * @return mixed zh_CN zh_TW en
     * @throws \Exception
     */
    public function getUserInfo($token, $openid, $lang='zh_CN'){
        $param = [
            'access_token' => $token,
            'openid' => $openid,
            'lang' => $lang
        ];
        $info =  self::http($this->oauthApiURL.'/userinfo',$param);
        return json_decode($info, true);
    }

    public function getUserInfoClient($token, $openid, $lang='zh_CN') {
        //https://api.weixin.qq.com/cgi-bin/user/info?
        $param = [
            'access_token' => $token,
            'openid' => $openid,
            'lang' => $lang
        ];
        $info =  self::http('https://api.weixin.qq.com/cgi-bin/user/info',$param);
        return json_decode($info, true);
    }

    /**
     * 群发预览接口
     * @param $msgType
     * @param $openid
     * @param $content
     * @return mixed
     */
    public function massPreview($msgType,$openid,$content){
        $data = [
            'touser' => $openid,
            $msgType => ['content'=>$content],
            'msgtype' => $msgType,
        ];
        return $this->apiRequest('message/mass/preview',$data);
    }

    /**
     * 群发消息
     * @param $msgType
     * @param $group_id
     * @param $content
     * @return mixed
     */
    public function massSendAll($msgType,$content,$group_id=null){
        $data = [
            'filter' => [
                'is_to_all'=>true,
                'group_id'=>$group_id,
            ],
            $msgType => ['content'=>$content],
            'msgtype'=>$msgType,
        ];
        return $this->apiRequest('message/mass/sendall',$data);
    }

    /**
     * 将一条长链接转成短链接。
     * @param $long_url
     * @return bool
     */
    public function createShortUrl($long_url){
        $data = [
            'action'=>'long2short',
            'long_url' => $long_url,
        ];
        $result = $this->apiRequest('shorturl',$data);
        return isset($result['errmsg']) && $result['errmsg'] == 'ok' ? $result['short_url'] : false;
    }

    /**
     * 发送模板消息
     * @param $toUser
     * @param $templateId
     * @param array $data
     * @param null $url
     * @return bool
     */
    public function sendTemplateMessage($toUser, $templateId, array $data, $url=null){
        $data = [
            'touser' => $toUser,
            'template_id' => $templateId,
            'url' => $url,
            'topcolor' => '#FF0000',
            'data' => $data
        ];
        $result = $this->apiRequest('message/template/send',$data);
        return isset($result['errmsg']) && $result['errmsg'] == 'ok' ? $result['msgid'] : false;
    }

    /**
     * 获取 jsapi_ticket
     * @return bool|mixed
     * @throws \Exception
     */
    public function getJsApiTicket(){
        $param = [
            'access_token' => $this->getAccessToken(),
            'type' => 'jsapi'
        ];
        $ticket = \Yii::$app->cache->get('jsapi_ticket');
        if (!empty($ticket)){
            return $ticket;
        }
        $result = self::http($this->apiURL.'/ticket/getticket',$param);
        $result = json_decode($result, true);
        if (isset($result['errmsg']) && $result['errmsg'] == 'ok'){
            $expire = $result['expires_in'] ? intval($result['expires_in'])-100 : 3600;
            \Yii::$app->cache->set('jsapi_ticket', $result['ticket'],$expire);
            return $result['ticket'];
        } else {
            return false;
        }
    }

    /**
     * 获取微信网页js引入的配置参数
     * @return array
     */
    public function getSignPackage(){
        $jsapiTicket = $this->getJsApiTicket();
        $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
        $url = "$protocol$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";
        $timestamp = time();
        $nonceStr = \Yii::$app->security->generateRandomString(16);
        $string = "jsapi_ticket=$jsapiTicket&noncestr=$nonceStr&timestamp=$timestamp&url=$url";
        $signature = sha1($string);
        $signPackage = [
            "appId"     => $this->appid,
            "nonceStr"  => $nonceStr,
            "timestamp" => $timestamp,
            "url"       => $url,
            "signature" => $signature,
            "rawString" => $string
        ];
        return $signPackage;
    }

    /**
     * 統一下單接口 預處理(第二次簽名)
     * @param $out_trade_no
     * @param string $body
     * @param $total_fee
     * @param $notify_url
     * @param $openid
     * @param string $attach
     * @param string $trade_type
     * @return bool
     * @throws \Exception
     */
    public function unifiedorder($out_trade_no, $body='', $total_fee, $notify_url, $openid, $attach='', $trade_type="JSAPI"){
        $url = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
        $params = [
            'appid' => $this->appid,
            'mch_id' => $this->mch_id,
            'nonce_str' => \Yii::$app->security->generateRandomString(),
            'body' => $body,
            'out_trade_no' => $out_trade_no,
            'total_fee' => $total_fee,
            'spbill_create_ip' => \Yii::$app->request->userIP,
            'notify_url' => $notify_url,
            'trade_type' => $trade_type,//JSAPI，NATIVE，APP
            'openid' => $openid,
            'attach' => $attach,
            'product_id' => '',
            'time_start' => date('YmdHis'),
            'time_expire' => date('YmdHis', (time() + 300)),
        ];
        \Yii::info($params,'WXLog');
        $sign = $this->paySign($params);//获取签名(第二次簽名)
        \Yii::info($sign,'WXLog');
        $params['sign'] = $sign;
        $data = self::arrayToXml($params);
        $resultXml = self::http($url,'',$data, 'POST');
        \Yii::info($resultXml,'WXLog');
        if($resultXml && $prepay_id = $this->checkPaySign($resultXml)) {
            return $prepay_id;
        }
        return false;
    }


    /**
     * 微信支付簽名（第二次簽名, 第三次簽名） 官方推薦MD5加密 并轉為大寫
     * @param $array
     * @return string
     * @throws \Exception
     */
    public function paySign($array){
        if(!is_array($array)) {
            throw new \Exception("pay sign data must be type array");
        }
        $paraFilter = self::paraFilter($array);
        $data = self::argSort($paraFilter);
        $str = self::createListString($data).'&key='.$this->key;
        return strtoupper(md5($str));
    }

    /**
     * 簽名檢查 通過則返回 prepay_id
     * @param $xml
     * @return bool
     * @throws \Exception
     */
    public function checkPaySign($xml){
        $arr = static::xmlToArray($xml);
        if (!isset($arr['sign'])) {
            throw new \Exception("sign can't empty");
        }
        $sign = $arr['sign'];
        unset($arr['sign']);
        //return $sign === $this->paySign($arr) ? true : false;
        if($sign === $this->paySign($arr)) {
            if($arr['result_code'] == 'SUCCESS') {
                return $arr['prepay_id'];
            }
            return false;
        }
        return false;
    }

    /**
     * 生成最後支付的請求數據
     * @param $prepay_id
     * @return array
     * @throws \Exception
     */
    public function payRequestData($prepay_id){
        $appid = $this->appid;
        $timeStamp = time();
        $nonceStr = \Yii::$app->security->generateRandomString();
        $package = "prepay_id=$prepay_id";
        $signType = "MD5";
        $data = [
            'appId' => $appid,
            'timeStamp' => "$timeStamp",
            'nonceStr' => $nonceStr,
            'package' => $package,
            'signType' => $signType
        ];
        $paySign = $this->paySign($data);
        $data['paySign'] = $paySign;
        return $data;
    }

    /**
     * 支付回調
     * @return array|bool|mixed
     * @throws \Exception
     */
    public function notify(){
        $requestData = empty($GLOBALS["HTTP_RAW_POST_DATA"]) ? '' : $GLOBALS["HTTP_RAW_POST_DATA"];
        if(!empty($requestData)){
            //\Yii::info($requestData,'WXLog');
            $arr = static::xmlToArray($requestData);
            if($arr['result_code'] == 'SUCCESS'){
                return $arr;
            }
            return false;
        }
        return false;
    }

    /**
     * 支付成功的回調通知
     * @return array|string
     * @throws \Exception
     */
    public function notifyReply(){
        $data = [
            'return_code' => 'SUCCESS',
            'return_msg' => 'OK'
        ];
        $data = self::arrayToXml($data);
        return $data;
    }

    /**
     * @param $name 商户名称
     * @param $openid 用户openid
     * @param $amount 付款金额
     * @param $num 红包发放总人数
     * @param $description 红包祝福语
     * @param $active_name 活动名称
     * @param $mark 备注
     * @return bool
     * @throws \Exception
     */
    public function sendRedPack($name,$openid,$amount,$num, $description, $active_name, $mark){
        $nonce_str = \Yii::$app->security->generateRandomString();
        $sign = '';
        $mch_billno = $this->mch_id.date('Ymd').time();
        $mch_id = $this->mch_id;
        $wxappid = $this->appid;
        $send_name = $name;
        $re_openid = $openid;
        $total_amount = $amount*1000;
        $total_num = $num;
        $wishing = $description;
        $client_ip = \Yii::$app->request->userIP;
        $act_name = $active_name;
        $remark = $mark;
        $data = [
            'nonce_str' => $nonce_str,
            'mch_billno' => $mch_billno,
            'mch_id' => $mch_id,
            'wxappid' => $wxappid,
            'send_name' => $send_name,
            're_openid' => $re_openid,
            'total_amount' => $total_amount,
            'total_num' => $total_num,
            'wishing' => $wishing,
            'client_ip' => $client_ip,
            'act_name' => $act_name,
            'remark' => $remark
        ];
        $data['sign'] = $this->paySign($data);
        $requestData = self::arrayToXml($data);
        $url = 'https://api.mch.weixin.qq.com/mmpaymkttransfers/sendredpack';
        $cert = '';
        $result = self::http($url,'',$requestData, 'POST', true);
        if ($result) {
            return $this->xmlToArray($result);
        }
        return false;
    }

    //微信連wifi
    public function getWifiQrcode($shop_id,$ssid, $img_id=1){
        $data = [
            'shop_id' => $shop_id,
            'ssid' => $ssid,
            'img_id' => $img_id
        ];
        $url = "https://api.weixin.qq.com/bizwifi/qrcode/get?access_token=".$this->getAccessToken();
        $data = self::json_encode($data);//自己封装的json_encode
        $result = self::http($url, '', $data, 'POST');
        return json_decode($result, true);
    }

    public function createMenu($menuArray){
        $data = self::json_encode($menuArray);
        $url = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=".$this->getAccessToken();
        $resul = self::http($url, '', $data, 'POST');
        return json_decode($resul, true);
    }

    /**
     * 過濾 sign
     * @param $para
     * @return array
     */
    public static function paraFilter($para) {
        $para_filter = array();
        while (list ($key, $val) = each ($para)) {
            if($key == "sign" || $val == "")continue;
            else	$para_filter[$key] = $para[$key];
        }
        return $para_filter;
    }

    /**
     * 重新排序
     * @param $para
     * @return mixed
     */
    public static function argSort($para) {
        ksort($para);
        reset($para);
        return $para;
    }

    /**
     * @param $name
     * @param string $data
     * @param string $method
     * @param string $param
     * @param bool|true $json
     * @return mixed
     * @throws \Exception
     */
    protected function apiRequest($name, $data='', $method='POST', $param='',$json=true){
        $params = ['access_token'=>$this->getAccessToken()];
        if(!empty($param) && is_array($param)){
            $params = array_merge($params, $param);
        }
        $url = $this->apiURL.'/'.$name;
        /*if($json && !empty($data)){
            array_walk_recursive($data, function(&$value){
                $value = urlencode($value);
            });
            $data = urldecode(json_encode($data));
        }*/
        $data = self::json_encode($data);//自己封装的json_encode
        $result = self::http($url, $params, $data, $method);
        return json_decode($result, true);

    }

    /**
     * @param $url
     * @param $param
     * @param string $data
     * @param string $method
     * @param bool $useCert
     * @return bool
     */
    private static function http($url, $param, $data='',$method='GET', $useCert = false){
        $opts = [
            CURLOPT_TIMEOUT => 30,
            CURLOPT_RETURNTRANSFER => 1,
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => false,
        ];
        $opts[CURLOPT_URL] = is_array($param) ? $url . '?' . http_build_query($param) : $url;
        if(strtoupper($method) == 'POST'){
            $opts[CURLOPT_POST] = 1;
            $opts[CURLOPT_POSTFIELDS] = $data;
            /*if(is_string($data)) {
                $opts[CURLOPT_HTTPHEADER] = [
                    'Content-Type: application/json; charset=utf-8',
                    'Content-Length:'.strlen($data),
                ];
            }*/
        }
        if ($useCert == true){
            $opts[CURLOPT_SSLCERTTYPE] = 'PEM';
            $opts[CURLOPT_SSLCERT] = dirname(__FILE__). DIRECTORY_SEPARATOR . 'apiclient_cert.pem';
            $opts[CURLOPT_SSLCERTTYPE] = 'PEM';
            $opts[CURLOPT_SSLKEY] = dirname(__FILE__). DIRECTORY_SEPARATOR . 'apiclient_key.pem';;
        }
        $ch = curl_init();
        curl_setopt_array($ch, $opts);
        $result = curl_exec($ch);
        //$error = curl_error($ch);
        $status = curl_getinfo($ch);
        curl_close($ch);
        if (isset($status['http_code']) && intval($status['http_code']) == 200) {
            return $result;
        }
        return false;

    }

    /**
     * @param $para
     * @return string
     */
    public static function createListString($para){
        $arg="";
        while( list($key, $val) = each($para) ){
            $arg.= $key."=".$val."&";
        }
        $arg = substr($arg, 0, count($arg)-2);
        if (get_magic_quotes_gpc()) {
            $arg = stripslashes($arg);
        }
        return $arg;
    }

    /**
     * 微信api不支持中文转义的json结构
     * @param $arr
     * @return string
     */
    public static function json_encode($arr) {
        if (count($arr) == 0) return "[]";
        $parts = array ();
        $is_list = false;
        //Find out if the given array is a numerical array
        $keys = array_keys ( $arr );
        $max_length = count ( $arr ) - 1;
        if (($keys [0] === 0) && ($keys [$max_length] === $max_length )) { //See if the first key is 0 and last key is length - 1
            $is_list = true;
            for($i = 0; $i < count ( $keys ); $i ++) { //See if each key correspondes to its position
                if ($i != $keys [$i]) { //A key fails at position check.
                    $is_list = false; //It is an associative array.
                    break;
                }
            }
        }
        foreach ( $arr as $key => $value ) {
            if (is_array ( $value )) { //Custom handling for arrays
                if ($is_list)
                    $parts [] = self::json_encode ( $value ); /* :RECURSION: */
                else
                    $parts [] = '"' . $key . '":' . self::json_encode ( $value ); /* :RECURSION: */
            } else {
                $str = '';
                if (! $is_list)
                    $str = '"' . $key . '":';
                //Custom handling for multiple data types
                if (!is_string ( $value ) && is_numeric ( $value ) && $value<2000000000)
                    $str .= $value; //Numbers
                elseif ($value === false)
                    $str .= 'false'; //The booleans
                elseif ($value === true)
                    $str .= 'true';
                else
                    $str .= '"' . addslashes ( $value ) . '"'; //All other things
                // :TODO: Is there any more datatype we should be in the lookout for? (Object?)
                $parts [] = $str;
            }
        }
        $json = implode ( ',', $parts );
        if ($is_list)
            return '[' . $json . ']'; //Return numerical JSON
        return '{' . $json . '}'; //Return associative JSON
    }

    public function arrayToXml($array, $header=true) {
        if (!is_array($array) && !$array instanceof Object) {
            throw new \Exception("\$array must be type array or Object");
        }
        $xml = $header === true ? '<xml>' : '';
        foreach ($array as $k => $v) {
            if (is_array($v)) {
                $xml .= '<' . $k . '>' . self::arrayToXml($v, $header) . '</' . $k . '>';
            } else {
                $xml .= '<' . $k . '><![CDATA[' . $v . ']]></' . $k . '>';
            }
        }
        $xml .= $header === true ? '</xml>' : '';
        return $xml;
    }

    public function xmlToArray($xml)
    {
        if (!function_exists("simplexml_load_string")) {
            throw new \Exception("need  function simplexml_load_string");
        }
        libxml_disable_entity_loader(true);
        $obj = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
        return empty($obj) ? [] : json_decode(json_encode($obj), true);
    }
}